__version__ = "4.1"

import json

from meshroom.core import desc

def findMetadata(d, keys, defaultValue):
    v = None
    for key in keys:
        v = d.get(key, None)
        k = key.lower()
        if v is not None:
            return v
        for dk, dv in d.items():
            dkm = dk.lower().replace(" ", "")
            if dkm == key.lower():
                return dv
            dkm = dkm.split(":")[-1]
            dkm = dkm.split("/")[-1]
            if dkm == k:
                return dv
    return defaultValue


class LdrToHdrMerge(desc.AVCommandLineNode):
    commandLine = 'aliceVision_LdrToHdrMerge {allParams}'
    size = desc.DynamicNodeSize('input')
    parallelization = desc.Parallelization(blockSize=2)
    commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}'

    category = 'Panorama HDR'
    documentation = '''
Merge LDR images into HDR images.
'''

    inputs = [
        desc.File(
            name="input",
            label="SfMData",
            description="Input SfMData file.",
            value="",
            uid=[0],
        ),
        desc.File(
            name="response",
            label="Response File",
            description="Response file.",
            value="",
            uid=[0],
        ),
        desc.IntParam(
            name="userNbBrackets",
            label="Number Of Brackets",
            description="Number of exposure brackets per HDR image (0 for automatic detection).",
            value=0,
            range=(0, 15, 1),
            uid=[],
            group="user",  # not used directly on the command line
        ),
        desc.IntParam(
            name="nbBrackets",
            label="Automatic Nb Brackets",
            description="Number of exposure brackets used per HDR image.\n"
                        "It is detected automatically from input Viewpoints metadata if 'userNbBrackets'\n"
                        "is 0, else it is equal to 'userNbBrackets'.",
            value=0,
            range=(0, 10, 1),
            uid=[0],
        ),
        desc.BoolParam(
            name="offsetRefBracketIndexEnabled",
            label="Manually Specify Ref Bracket",
            description="Manually specify the reference bracket index to control the exposure of the HDR image.",
            value=False,
            uid=[0],
            group="user",  # not used directly on the command line
        ),
        desc.IntParam(
            name="offsetRefBracketIndex",
            label="Offset Ref Bracket Index",
            description="0 to use the center bracket.\n"
                        "+N to use a more exposed bracket or -N to use a less exposed bracket.",
            value=1,
            range=(-4, 4, 1),
            uid=[0],
            enabled= lambda node: (node.nbBrackets.value != 1 and node.offsetRefBracketIndexEnabled.value),
        ),
        desc.FloatParam(
            name="meanTargetedLumaForMerging",
            label="Targeted Luminance For Merging",
            description="Expected mean luminance of the HDR images used to compute the final panorama.",
            value=0.4,
            range=(0.0, 1.0, 0.01),
            uid=[0],
            enabled= lambda node: (node.nbBrackets.value != 1 and not node.offsetRefBracketIndexEnabled.value),
        ),
        desc.BoolParam(
            name="byPass",
            label="Bypass",
            description="Bypass HDR creation and use the medium bracket as the source for the next steps.",
            value=False,
            uid=[0],
            enabled= lambda node: node.nbBrackets.value != 1,
        ),
        desc.BoolParam(
            name="keepSourceImageName",
            label="Keep Source Image Name",
            description="Keep the filename of the input image selected as central image for the output image filename.",
            value=False,
            uid=[0],
        ),
        desc.ChoiceParam(
            name="fusionWeight",
            label="Fusion Weight",
            description="Weight function used to fuse all LDR images together:\n"
                        " - gaussian\n"
                        " - triangle\n"
                        " - plateau",
            value="gaussian",
            values=["gaussian", "triangle", "plateau"],
            exclusive=True,
            uid=[0],
            enabled= lambda node: node.byPass.enabled and not node.byPass.value,
        ),
        desc.IntParam(
            name="channelQuantizationPower",
            label="Channel Quantization Power",
            description="Quantization level like 8 bits or 10 bits.",
            value=10,
            range=(8, 14, 1),
            uid=[0],
            advanced=True,
            enabled= lambda node: node.byPass.enabled and not node.byPass.value,
        ),
        desc.ChoiceParam(
            name="workingColorSpace",
            label="Working Color Space",
            description="Allows you to choose the color space in which the data are processed.",
            value="sRGB",
            values=["sRGB", "Linear", "ACES2065-1", "ACEScg", "no_conversion"],
            exclusive=True,
            uid=[0],
            enabled= lambda node: node.byPass.enabled and not node.byPass.value,
        ),
        desc.BoolParam(
            name="enableHighlight",
            label="Enable Highlight",
            description="Enable highlights correction.",
            value=False,
            uid=[0],
            group="user",  # not used directly on the command line
            enabled= lambda node: node.byPass.enabled and not node.byPass.value,
        ),
        desc.FloatParam(
            name="highlightCorrectionFactor",
            label="Highlights Correction",
            description="Pixels saturated in all input images have a partial information about their real luminance.\n"
                        "We only know that the value should be >= to the standard HDRfusion.\n"
                        "This parameter allows to perform a post-processing step to put saturated pixels to a constant\n"
                        "value defined by the `highlightsMaxLuminance` parameter.\n"
                        "This parameter is float to enable to weight this correction.",
            value=1.0,
            range=(0.0, 1.0, 0.01),
            uid=[0],
            enabled= lambda node: node.enableHighlight.enabled and node.enableHighlight.value,
        ),
        desc.FloatParam(
            name="highlightTargetLux",
            label="Highlight Target Luminance (Lux)",
            description="This is an arbitrary target value (in Lux) used to replace the unknown luminance value of the saturated pixels.\n"
                        "\n"
                        "Some Outdoor Reference Light Levels:\n"
                        " - 120,000 lux: Brightest sunlight\n"
                        " - 110,000 lux: Bright sunlight\n"
                        " - 20,000 lux: Shade illuminated by entire clear blue sky, midday\n"
                        " - 1,000 lux: Typical overcast day, midday\n"
                        " - 400 lux: Sunrise or sunset on a clear day\n"
                        " - 40 lux: Fully overcast, sunset/sunrise\n"
                        "\n"
                        "Some Indoor Reference Light Levels:\n"
                        " - 20000 lux: Max Usually Used Indoor\n"
                        " - 750 lux: Supermarkets\n"
                        " - 500 lux: Office Work\n"
                        " - 150 lux: Home\n",
            value=120000.0,
            range=(1000.0, 150000.0, 1.0),
            uid=[0],
            enabled= lambda node: node.enableHighlight.enabled and node.enableHighlight.value and node.highlightCorrectionFactor.value != 0,
        ),
        desc.ChoiceParam(
            name="storageDataType",
            label="Storage Data Type",
            description="Storage image data type:\n"
                        " - float: Use full floating point (32 bits per channel).\n"
                        " - half: Use half float (16 bits per channel).\n"
                        " - halfFinite: Use half float, but clamp values to avoid non-finite values.\n"
                        " - auto: Use half float if all values can fit, else use full float.",
            value="float",
            values=["float", "half", "halfFinite", "auto"],
            exclusive=True,
            uid=[0],
        ),
        desc.ChoiceParam(
            name="verboseLevel",
            label="Verbose Level",
            description="Verbosity level (fatal, error, warning, info, debug, trace).",
            value="info",
            values=["fatal", "error", "warning", "info", "debug", "trace"],
            exclusive=True,
            uid=[],
        )
    ]

    outputs = [
        desc.File(
            name="outputFolder",
            label="Folder",
            description="Path to the folder containing the merged HDR images.",
            value=desc.Node.internalFolder,
            uid=[],
            group="",  # do not export on the command line
        ),
        desc.File(
            name="outSfMData",
            label="SfMData",
            description="Path to the output SfMData file.",
            value=desc.Node.internalFolder + "sfmData.sfm",
            uid=[],
        )
    ]

    @classmethod
    def update(cls, node):
        if not isinstance(node.nodeDesc, cls):
            raise ValueError("Node {} is not an instance of type {}".format(node, cls))
        # TODO: use Node version for this test
        if 'userNbBrackets' not in node.getAttributes().keys():
            # Old version of the node
            return
        if node.userNbBrackets.value != 0:
            node.nbBrackets.value = node.userNbBrackets.value
            return
        # logging.info("[LDRToHDR] Update start: version:" + str(node.packageVersion))
        cameraInitOutput = node.input.getLinkParam(recursive=True)
        if not cameraInitOutput:
            node.nbBrackets.value = 0
            return
        if not cameraInitOutput.node.hasAttribute('viewpoints'):
            if cameraInitOutput.node.hasAttribute('input'):
                cameraInitOutput = cameraInitOutput.node.input.getLinkParam(recursive=True)
        if cameraInitOutput and cameraInitOutput.node and cameraInitOutput.node.hasAttribute('viewpoints'):
            viewpoints = cameraInitOutput.node.viewpoints.value
        else:
            # No connected CameraInit
            node.nbBrackets.value = 0
            return

        # logging.info("[LDRToHDR] Update start: nb viewpoints:" + str(len(viewpoints)))
        inputs = []
        for viewpoint in viewpoints:
            jsonMetadata = viewpoint.metadata.value
            if not jsonMetadata:
                # no metadata, we cannot found the number of brackets
                node.nbBrackets.value = 0
                return
            d = json.loads(jsonMetadata)
            fnumber = findMetadata(d, ["FNumber", "Exif:ApertureValue", "ApertureValue", "Aperture"], "")
            shutterSpeed = findMetadata(d, ["Exif:ShutterSpeedValue", "ShutterSpeedValue", "ShutterSpeed"], "")
            iso = findMetadata(d, ["Exif:ISOSpeedRatings", "ISOSpeedRatings", "ISO"], "")
            if not fnumber and not shutterSpeed:
                # If one image without shutter or fnumber, we cannot found the number of brackets.
                # We assume that there is no multi-bracketing, so nothing to do.
                node.nbBrackets.value = 1
                return
            inputs.append((viewpoint.path.value, (fnumber, shutterSpeed, iso)))
        inputs.sort()

        exposureGroups = []
        exposures = []
        for path, exp in inputs:
            if exposures and exp != exposures[-1] and exp == exposures[0]:
                exposureGroups.append(exposures)
                exposures = [exp]
            else:
                exposures.append(exp)
        exposureGroups.append(exposures)
        exposures = None
        bracketSizes = set()
        if len(exposureGroups) == 1:
            if len(set(exposureGroups[0])) == 1:
                # Single exposure and multiple views
                node.nbBrackets.value = 1
            else:
                # Single view and multiple exposures
                node.nbBrackets.value = len(exposureGroups[0])
        else:
            for expGroup in exposureGroups:
                bracketSizes.add(len(expGroup))
            if len(bracketSizes) == 1:
                node.nbBrackets.value = bracketSizes.pop()
                # logging.info("[LDRToHDR] nb bracket size:" + str(node.nbBrackets.value))
            else:
                node.nbBrackets.value = 0
        # logging.info("[LDRToHDR] Update end")

